package org.unidata.mdm.rest.v1.data.service.merge;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.ws.rs.Consumes;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.lang3.ObjectUtils;
import org.unidata.mdm.data.context.GetRequestContext;
import org.unidata.mdm.data.context.MergeRelationsRequestContext;
import org.unidata.mdm.data.context.MergeRequestContext;
import org.unidata.mdm.data.context.PreviewRequestContext;
import org.unidata.mdm.data.dto.GetRecordDTO;
import org.unidata.mdm.data.dto.MergeRecordsDTO;
import org.unidata.mdm.rest.system.ro.DetailedErrorResponseRO;
import org.unidata.mdm.rest.system.ro.details.ErrorDetailsRO;
import org.unidata.mdm.rest.system.ro.details.InfoDetailsRO;
import org.unidata.mdm.rest.system.ro.details.ResultDetailsRO;
import org.unidata.mdm.rest.v1.data.converter.EtalonPreviewConverter;
import org.unidata.mdm.rest.v1.data.ro.WinnerRO;
import org.unidata.mdm.rest.v1.data.ro.merge.MergePreviewRO;
import org.unidata.mdm.rest.v1.data.ro.merge.MergeRequestRO;
import org.unidata.mdm.rest.v1.data.ro.merge.MergeResultRO;
import org.unidata.mdm.rest.v1.data.ro.records.DataRecordRO;
import org.unidata.mdm.rest.v1.data.ro.records.EtalonRecordRO;
import org.unidata.mdm.rest.v1.data.ro.records.ExtendedRecordRO;
import org.unidata.mdm.rest.v1.data.ro.records.NestedRecordRO;
import org.unidata.mdm.rest.v1.data.ro.records.UpsertRequestRO;
import org.unidata.mdm.rest.v1.data.service.AbstractDataRestService;
import org.unidata.mdm.system.type.runtime.MeasurementContextName;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;

/**
 * Merge entities service
 *
 * @author Alexandr Serov
 * @since 14.10.2020
 **/
@Path("/merge")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
@Tag(name = "merge")
public class DataMergeRestService extends AbstractDataRestService {

    private static final String MERGE_TAG = "merge";

    /**
     * Merge.
     *
     * @param mergeRequest the merge request
     * @return the response
     */
    @POST
    @Path("/apply")
    @Operation(
        summary = "Merge duplicates.",
        description = "Merge multiple duplicate records in favor of one record.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = MergeRequestRO.class))),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "400"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = MergeResultRO.class)), responseCode = "200")
        }, tags = MERGE_TAG
    )
    public MergeResultRO merge(MergeRequestRO mergeRequest) {
        return executeMerge(mergeRequest);
    }

    /**
     * Merge preview.
     *
     * @param mergeRequest the merge request
     * @return the response
     */
    @POST
    @Path("/preview")
    @Operation(
        summary = "Merge preview.",
        description = "Merges the benchmarks virtually and returns a materialized view of the merged benchmarks.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = MergeRequestRO.class))),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "400"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = DetailedErrorResponseRO.class)), responseCode = "500"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = MergePreviewRO.class)), responseCode = "200"),
        }, tags = MERGE_TAG
    )
    public MergePreviewRO mergePreview(MergeRequestRO mergeRequest) {
        return executeMergePreview(mergeRequest);
    }


    private MergePreviewRO executeMergePreview(MergeRequestRO mergeRequest) {
        Objects.requireNonNull(mergeRequest, "MergeRequestRO can't be null");
        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_MERGE_PREVIEW);
        MeasurementPoint.start();
        try {
            List<String> etalonsIds = ObjectUtils.defaultIfNull(mergeRequest.getEtalonIds(), Collections.emptyList());
            String winnerId = mergeRequest.getWinnerEtalonId();
            if (winnerId == null) {
                mergeRequest.setWinnerEtalonId(removeLastElementIfPreset(etalonsIds));
            }
            MergePreviewRO result = new MergePreviewRO();
            ExtendedRecordRO newRecord = calculateMergedEtalon(mergeRequest);
            ExtendedRecordRO oldRecord = calculateMergedEtalon(mergeRequest);
            mergeRequest.setWinners(null);

            result.setManualWinners(mergeRequest.getWinners());
            result.setManualDataRecord(readEtalonRecord(newRecord));
            result.setDataRecord(readEtalonRecord(oldRecord));
            result.setWinnerEtalonId(oldRecord.getWinnerEtalonId());

            Optional.ofNullable(newRecord.getAttributeWinnersMap()).map(autoWinners -> {
                Map<String, WinnerRO> auto = new HashMap<>();
                autoWinners.forEach((k, v) -> {
                    WinnerRO winner = auto.get(v);
                    if (winner == null) {
                        auto.put(v, winner = new WinnerRO());
                        winner.setEtalonId(v);
                    }
                    winner.getPaths().add(k);
                });
                return auto;
            }).map(Map::values).map(ArrayList::new).ifPresent(result::setWinners);
            return result;
        } finally {
            MeasurementPoint.stop();
        }
    }

    private MergeResultRO executeMerge(MergeRequestRO mergeRequest) {
        Objects.requireNonNull(mergeRequest, "MergeRequest can't be null");
        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_MERGE);
        MeasurementPoint.start();
        try {
            Map<String, String> attributes = mapWinnerAttributes(mergeRequest.getWinners());
            List<String> etalonsIds = ObjectUtils.defaultIfNull(mergeRequest.getEtalonIds(), Collections.emptyList());
            // UI historically send ids twice
            etalonsIds.removeIf(k -> mergeRequest.getWinnerEtalonId().equals(k));
            String winnerEtalonId = mergeRequest.getWinnerEtalonId();
            MergeResultRO result = new MergeResultRO();
            result.setEtalonId(winnerEtalonId);
            MergeRecordsDTO mergeRecords = dataRecordsService.merge(MergeRequestContext.builder()
                .etalonKey(winnerEtalonId)
                .fragment(MergeRelationsRequestContext.builder().applyToAll(true).build())
                .duplicates(etalonsIds.stream().map(id -> GetRequestContext.builder()
                    .etalonKey(id)
                    .manualMergeAttrs(attributes)
                    .build()).collect(Collectors.toList()))
                .manual(true)
                .build());
            ResultDetailsRO details = errorsToDetails(mergeRecords.getErrors());
            if (!attributes.isEmpty()) {
                UpsertRequestRO upsertRequest = new UpsertRequestRO();
                ExtendedRecordRO newRecord = calculateMergedEtalon(mergeRequest);
                DataRecordRO record = readDataRecord(newRecord);
                record.setEntityName(mergeRequest.getEntityName());
                record.setEtalonId(winnerEtalonId);
                upsertRequest.setDataRecord(record);
                mergeResultDetails(details, executeUpsert(upsertRequest).getDetails());
            }
            result.setDetails(details);
            return result;
        } finally {
            MeasurementPoint.stop();
        }
    }

    private void mergeResultDetails(ResultDetailsRO target, ResultDetailsRO source) {
        Optional.ofNullable(source.getError()).ifPresent(src -> {
            List<ErrorDetailsRO> errors = Optional.ofNullable(target.getError()).orElseGet(ArrayList::new);
            errors.addAll(src);
            target.setError(errors);
        });
        Optional.ofNullable(source.getWarning()).ifPresent(src -> {
            List<InfoDetailsRO> warnings = Optional.ofNullable(target.getWarning()).orElseGet(ArrayList::new);
            warnings.addAll(src);
            target.setWarning(warnings);
        });
        Optional.ofNullable(source.getInfo()).ifPresent(src -> {
            List<InfoDetailsRO> warnings = Optional.ofNullable(target.getInfo()).orElseGet(ArrayList::new);
            warnings.addAll(src);
            target.setWarning(warnings);
        });
    }

    /**
     * Calculate merged etalon.
     *
     * @param mergeRequest the merge request
     * @return the extended record RO
     */
    private ExtendedRecordRO calculateMergedEtalon(MergeRequestRO mergeRequest) {
        Map<String, String> attributes = mapWinnerAttributes(mergeRequest.getWinners());
        List<String> etalonsIds = ObjectUtils.defaultIfNull(mergeRequest.getEtalonIds(), Collections.emptyList());
        String etalonKey = etalonsIds.isEmpty() ? mergeRequest.getWinnerEtalonId() : removeLastElementIfPreset(etalonsIds);
        GetRecordDTO previewRecord = dataRecordsService.preview(PreviewRequestContext.builder()
            .attributes(attributes)
            .etalonKey(etalonKey)
            .duplicates(etalonsIds.stream().map(etalonId -> GetRequestContext.builder()
                .etalonKey(etalonId)
                .build()).collect(Collectors.toList()))
            .forDate(new Date())
            .forLastUpdate(null)
            .build());
        ExtendedRecordRO extendedRecord = EtalonPreviewConverter.convert(previewRecord);
        if (attributes.isEmpty()) {
            Map<String, String> winners = ObjectUtils.defaultIfNull(extendedRecord.getAttributeWinnersMap(), Collections.emptyMap());
            if (!winners.isEmpty()) {
                winners.values().stream().findFirst().ifPresent(extendedRecord::setWinnerEtalonId);
            }
        } else {
            extendedRecord.setWinnerEtalonId(etalonKey);
        }
        return extendedRecord;
    }

    private DataRecordRO readDataRecord(ExtendedRecordRO extendedRecord) {
        EtalonRecordRO etalonRecord = readEtalonRecord(extendedRecord);
        DataRecordRO dataRecordRO = new DataRecordRO();
        dataRecordRO.setCodeAttributes(etalonRecord.getCodeAttributes());
        dataRecordRO.setSimpleAttributes(etalonRecord.getSimpleAttributes());
        dataRecordRO.setComplexAttributes(etalonRecord.getComplexAttributes());
        dataRecordRO.setArrayAttributes(etalonRecord.getArrayAttributes());
        dataRecordRO.setEntityName(etalonRecord.getEntityName());
//
        return dataRecordRO;
    }

    private EtalonRecordRO readEtalonRecord(ExtendedRecordRO extendedRecord) {
        NestedRecordRO nestedRecordRO = extendedRecord.getRecord();
        if (nestedRecordRO instanceof EtalonRecordRO) {
            return (EtalonRecordRO) nestedRecordRO;
        } else {
            throw new IllegalStateException("Illegal record type: " + nestedRecordRO);
        }
    }

    private static <T> T removeLastElementIfPreset(List<T> list) {
        return list != null && !list.isEmpty() ? list.remove(list.size() - 1) : null;
    }

    private Map<String, String> mapWinnerAttributes(List<WinnerRO> winners) {
        return Optional.ofNullable(winners).map(values -> {
            Map<String, String> result = new HashMap<>();
            for (WinnerRO winner : winners) {
                String etalonId = winner.getEtalonId();
                List<String> paths = ObjectUtils.defaultIfNull(winner.getPaths(), Collections.emptyList());
                paths.forEach(path -> result.put(path, etalonId));
            }
            return result;
        }).orElse(Collections.emptyMap());
    }

}
